# frozen_string_literal: true

require 'fileutils'
require 'wpxf/helpers/export'

# Provides reusable functionality for file download modules.
module Wpxf::WordPress::FileDownload
  include Wpxf
  include Wpxf::Db::Loot
  include Wpxf::Helpers::Export

  # Initialize a new instance of {FileDownload}
  def initialize
    super
    return unless register_remote_file_option?

    _update_info_without_validation(
      desc: %(
        This module exploits a vulnerability which allows you to
        download any arbitrary file (relative to #{working_directory})
        accessible by the user the web server is running as.
      )
    )

    register_option(
      StringOption.new(
        name: 'remote_file',
        desc: 'The path to the remote file',
        required: true,
        default: default_remote_file_path
      )
    )
  end

  def register_remote_file_option?
    true
  end

  # @return [String, nil] a custom description to use when storing the loot item.
  def loot_description; end

  # @return [String] the working directory of the vulnerable file.
  def working_directory; end

  # @return [String] the default remote file path.
  def default_remote_file_path; end

  # @return [String] the URL of the vulnerable file used to download remote files.
  def downloader_url; end

  # @return [Hash] the params to be used when requesting the download file.
  def download_request_params; end

  # @return [Hash, String] the body to be used when requesting the download file.
  def download_request_body; end

  # @return [Symbol] the HTTP method to use when requesting the download file.
  def download_request_method
    :get
  end

  # @return [String] the path to the remote file.
  def remote_file
    normalized_option_value('remote_file')
  end

  # Validate the contents of the requested file.
  # @param content [String] the file contents.
  # @return [Boolean] true if valid.
  def validate_content(content)
    true
  end

  # A task to run before the download starts.
  # @return [Boolean] true if pre-download operations were successful.
  def before_download
    true
  end

  # @return [String] the file extension to use when downloading the file.
  def file_extension
    ''
  end

  # @return [Integer] the expected HTTP code for a successful download.
  def expected_http_code
    200
  end

  # Handles an occurrence of an unexpected result.
  # @param code [Integer] the returned HTTP code.
  # @return [Boolean] true if the code should be ignored, false if the module should fail.
  def handle_unexpected_http_code(code)
    emit_error "Server responded with code #{code}"
    false
  end

  # @return [String] the type of file downloaded by the module.
  def file_category
    'unknown'
  end

  # Run the module.
  # @return [Boolean] true if successful.
  def run
    _validate_implementation

    return false unless super
    return false unless before_download

    @downloaded_filename = generate_unique_filename(file_extension)
    emit_info 'Downloading file...'
    res = download_file(_build_request_opts(@downloaded_filename))

    return false unless _validate_result(res)
    unless validate_content(res.body)
      FileUtils.rm @downloaded_filename, force: true
      return false
    end

    emit_success "Downloaded file to #{@downloaded_filename}"
    _store_file_as_loot

    true
  end

  # @return [String] returns the path the file was downloaded to.
  attr_reader :downloaded_filename

  private

  def _store_file_as_loot
    desc = loot_description

    if desc.nil? && register_remote_file_option?
      desc = "Remote file: #{File.basename(remote_file)[0..85]}"
    end

    desc = '' if desc.nil?
    store_loot downloaded_filename, desc[0..99], file_category
  end

  def _validate_implementation
    return unless register_remote_file_option?
    raise 'A value must be specified for #working_directory' unless working_directory
  end

  def _validate_result(res)
    if res.nil? || res.timed_out?
      emit_error 'Request timed out, try increasing the http_client_timeout'
      return false
    end

    return true unless res.code != expected_http_code
    handle_unexpected_http_code(res.code)
  end

  def _build_request_opts(filename)
    {
      method: download_request_method,
      url: downloader_url,
      params: download_request_params,
      body: download_request_body,
      cookie: session_cookie,
      local_filename: filename
    }
  end
end
